package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.product.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.sql.Time;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

    @Autowired
    StringRedisTemplate stringRedisTemplate; //SpringBoot自动化配置


    /**
     *  synchronized 或  Lock  都是本地锁。只能解决单个进程内多线程并发安全问题。如果是多个进程本地锁就失效了。
     *
     *  分布式锁失效：使用redis来设置共享标记来实现分布式锁。   setnx lock lock
     *
     *  问题1：setnx刚好获取到锁，业务逻辑出现异常，导致锁无法释放
     *        解决办法：加锁时设置过期时间。必须保证原子性。解决死锁问题。
     *
     *  问题2：可能会释放其他服务器(进程)的锁。
     *        解决办法：
     *          释放锁时判断锁的值是否是自己加锁时设置的值，是才解锁。不是的话就不要解锁了(锁由于超时已经释放过了)。
     *          注意：解锁时，判断和解锁 不是原子的，释放锁依然可能出现释放别人的锁。
     *              解决办法：用Lua脚本保证原子性。
     *
     *  问题3： 分布式锁如何实现可重入性
     *          解决办法：使用 redis hash 类型来解决。封装了两个方法，用于加锁和解锁。测试可重入锁
     */
    @Override
    public void testLock() {

        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        Boolean ifAbsent = this.tryLock("lock",uuid,30L);

        if(ifAbsent){ //抢到分布式锁。

            //假设执行业务时间7秒
            // 查询Redis中的num值
            String value = (String)this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
                return ;
            }
            // 有值就转成成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num)); //  java 中  ++ 操作不是原子的。

            //this.testSubLock(uuid);

            try {
                TimeUnit.SECONDS.sleep(60); //业务逻辑执行时长60秒；
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            this.unlock("lock",uuid);
            //this.unlock("lock",uuid); //如果锁已经释放，再去释放锁，会抛异常。
        }

    }

    private void testSubLock(String uuid) {
        Boolean ifAbsent = this.tryLock("lock",uuid,300L);
        System.out.println("可重入锁获取成功....");
        this.unlock("lock",uuid);
    }


    //加锁，具备可重入锁特性。
    private Boolean tryLock(String lockName, String uuid, Long expire){
        String script = "if(redis.call('exists' , KEYS[1]) == 0 ) then" +
                " redis.call('hset' , KEYS[1] ,ARGV[1] , 1)" +
                " redis.call('expire' , KEYS[1] , ARGV[2])" +
                "  return 1" +
                " elseif(redis.call('hexists' , KEYS[1],ARGV[1])==1) then" +
                " redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1)" +
                "  return 1" +
                " else" +
                "  return 0" +
                " end";
        if (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
            try {
                // 没有获取到锁，重试
                Thread.sleep(200);
                tryLock(lockName, uuid, expire);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        renewTime(lockName,uuid,expire);

        // 获取到锁，返回true
        return true;
    }


    //解锁
    private void unlock(String lockName, String uuid){
        String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
                "    return nil;" +
                "end;" +
                "if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
                "    return 0;" +
                "else" +
                "    redis.call('del', KEYS[1]);" +
                "    return 1;" +
                "end;";
        // 这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中，三个返回值含义如下：
        // 1 代表解锁成功，锁被释放
        // 0 代表可重入次数被减 1
        // null 代表其他线程尝试解锁，解锁失败
        Long result = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),  Arrays.asList(lockName), uuid);
        // 如果未返回值，代表尝试解其他线程的锁
        if (result == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: "
                    + lockName + " with request: "  + uuid);
        }
    }



    /**
     * 锁延期
     * 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,因此在此过程中,其他线程无法获取到锁,保证了线程安全性
     * @param lockName
     * @param expire 单位：毫秒
     */
    private void renewTime(String lockName, String uuid, Long expire){
        String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
        new Thread(() -> {
            while (this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
                try {
                    // 到达过期时间的2/3时间，自动续期
                    Thread.sleep(expire* 1000 / 3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
